threedim: PrimitiveCloud point-cloud / Gaussian-splat family (from #2109 stack) - #2123
Open
jcelerier wants to merge 104 commits into
Open
threedim: PrimitiveCloud point-cloud / Gaussian-splat family (from #2109 stack)#2123jcelerier wants to merge 104 commits into
jcelerier wants to merge 104 commits into
Conversation
jcelerier
force-pushed
the
planf2/pointcloud
branch
from
July 17, 2026 03:29
2b22ce2 to
3f31f9d
Compare
jcelerier
force-pushed
the
planf/objects
branch
2 times, most recently
from
July 17, 2026 11:58
eb14ad5 to
899f99b
Compare
jcelerier
force-pushed
the
planf2/pointcloud
branch
from
July 17, 2026 11:58
3f31f9d to
ebeb8f4
Compare
jcelerier
added a commit
that referenced
this pull request
Jul 17, 2026
… flush test_unit_threedim_loaders: AssetLoader/GltfParser/FbxParser/TinyObj/Ply/Vcg + the PrimitiveCloud parsers, compiled straight into the test (plugin is -fvisibility=hidden). test_unit_primitivecloud: PLY/.splat/.spz parsers + scene helpers. ObjectGallery: explicit llvm profile flush on _Exit + SEGV/ABRT so coverage runs of the render-regression harness still emit .profraw (atop this branch's render-regression mode). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
jcelerier
force-pushed
the
planf2/pointcloud
branch
from
July 17, 2026 14:10
ebeb8f4 to
fc9ac65
Compare
jcelerier
added a commit
that referenced
this pull request
Jul 17, 2026
… flush test_unit_threedim_loaders: AssetLoader/GltfParser/FbxParser/TinyObj/Ply/Vcg + the PrimitiveCloud parsers, compiled straight into the test (plugin is -fvisibility=hidden). test_unit_primitivecloud: PLY/.splat/.spz parsers + scene helpers. ObjectGallery: explicit llvm profile flush on _Exit + SEGV/ABRT so coverage runs of the render-regression harness still emit .profraw (atop this branch's render-regression mode). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
jcelerier
force-pushed
the
planf2/pointcloud
branch
from
July 17, 2026 14:29
fc9ac65 to
ec5deba
Compare
jcelerier
added a commit
that referenced
this pull request
Jul 17, 2026
… flush test_unit_threedim_loaders: AssetLoader/GltfParser/FbxParser/TinyObj/Ply/Vcg + the PrimitiveCloud parsers, compiled straight into the test (plugin is -fvisibility=hidden). test_unit_primitivecloud: PLY/.splat/.spz parsers + scene helpers. ObjectGallery: explicit llvm profile flush on _Exit + SEGV/ABRT so coverage runs of the render-regression harness still emit .profraw (atop this branch's render-regression mode). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
jcelerier
force-pushed
the
planf2/pointcloud
branch
2 times, most recently
from
July 18, 2026 13:37
7855239 to
ef58365
Compare
jcelerier
force-pushed
the
planf2/pointcloud
branch
from
July 18, 2026 14:17
ef58365 to
da52f0e
Compare
jcelerier
force-pushed
the
planf/objects
branch
2 times, most recently
from
July 18, 2026 14:23
3d06f50 to
3fe10b1
Compare
jcelerier
force-pushed
the
planf2/pointcloud
branch
from
July 18, 2026 14:23
da52f0e to
c067bbe
Compare
jcelerier
force-pushed
the
planf2/pointcloud
branch
2 times, most recently
from
July 19, 2026 03:14
337c225 to
5c1950c
Compare
jcelerier
force-pushed
the
planf2/pointcloud
branch
from
July 19, 2026 21:59
5c1950c to
f22647e
Compare
jcelerier
force-pushed
the
planf/objects
branch
2 times, most recently
from
July 20, 2026 23:09
df39bc1 to
c59c898
Compare
jcelerier
force-pushed
the
planf2/pointcloud
branch
from
July 20, 2026 23:09
f22647e to
c19744e
Compare
jcelerier
force-pushed
the
planf2/pointcloud
branch
from
July 21, 2026 04:55
c19744e to
2e67992
Compare
The scene testers name Primitive cube, Camera and DirectionalLight in their `Wire:` clauses. Those are halp processes in score_plugin_threedim, and oscr::GfxNode<T> wraps one into a score::gfx node given only a score::DocumentContext -- no interval, no executor, no createProcess command. score::gfx::ScenePreprocessorNode (the SceneFlattener) was always constructible; what it lacked was an ossia::scene_spec source, which a Crousti-wrapped producer supplies. Lands in this branch rather than with the other sweeps because it compiles Camera.cpp and Light.cpp straight into the target -- the plugin is built with hidden visibility -- and both arrive here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the point-cloud objects: a PLY parser, a .splat binary reader, an SPZ codec (compressed 3D Gaussian splats, via the new spz + zstd submodules), a scene-from-cloud node and a format override, plus the TagAs helper. The asset loader routes .ply/.splat/.spz through them. The existing Splat render node and Splat loader are kept alongside the new family rather than retired, so both remain available. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
ObjectGallery --render <name> : add one object, route its texture output to
an offscreen window, play, and exit 0 if it
rendered a full run without crashing (a
render-path crash kills the process, which
the driver attributes to that object).
render-sweep.sh [filter] : run --render for each object on BOTH GL
backends (llvmpipe and the machine's GPU via
X11) and tally crashes.
Texture objects (ISF, images, model display, ...) render clean on both
backends. Two follow-ups: geometry-only objects need a display chain (they
currently abort when run standalone), and the offscreen readback does not yet
composite the routed texture so the non-blank check is informational.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
ObjectGallery gains a --shader <file> mode: it loads a shader tester (.fs/.frag/.glsl as an ISF Shader, .cs/.comp/.csf as a Compute Shader) into a process, routes its texture output to an offscreen window and renders it, exiting non-zero if it fails to compile or crashes. csf-sweep.sh drives that over the csf-examples corpus on BOTH GL backends (llvmpipe and the machine's GPU via X11). On the csf-testers set, 100/121 shaders render on both backends; the remaining 21 are geometry-producing compute shaders that need a downstream raster stage to display (see the corpus's tests.txt) — chaining them is the next step. Also fixes the sweep drivers to pass per-backend env via `env`, since bash does not treat VAR=val from an expanded "$@" as assignments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
test_unit_threedim_loaders: AssetLoader/GltfParser/FbxParser/TinyObj/Ply/Vcg + the PrimitiveCloud parsers, compiled straight into the test (plugin is -fvisibility=hidden). test_unit_primitivecloud: PLY/.splat/.spz parsers + scene helpers. ObjectGallery: explicit llvm profile flush on _Exit + SEGV/ABRT so coverage runs of the render-regression harness still emit .profraw (atop this branch's render-regression mode). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Every allocation in the createRenderTarget family was a SCORE_ASSERT, so a backend declining a configuration -- an MSAA depth attachment it does not implement, a format it will not render to -- killed the process. That is the wrong failure for a runtime condition: a headless sweep dies on the first unsupported case instead of reporting it, and an interactive session loses the document. 37 asserts across the nine render-target functions now return an empty TextureRenderTarget, which operator bool() already reports as invalid, after releasing what that function allocated. `texture` is left alone in the overload that is handed one -- it belongs to the caller. SCORE_ASSERT(tex) stays: a null argument there is a broken caller, not a driver refusal. Returning an invalid target only moves the crash unless the callers cope, so the offscreen path is closed off too: buildPipeline returns an empty pipeline rather than asserting on the missing render pass, InvertYRenderer::init gives up when it has no target, and finishFrame skips the pass instead of handing a null target to beginPass. Measured on the case that used to abort (asset loader -> ModelDisplay -> Window, Qt 6.4): exit 133 with a core dump before, exit 0 now, with the reason in the log at each step. The 130-case corpus is unchanged: RENDER 89, BLANK 41, NORENDER 0, CRASH 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
7f007b8 covers the PLY / .splat / .spz parsers thoroughly, but none of the AssetLoader dispatch above them: the existing .ply section writes a mesh PLY, so only the FALSE branch of ply_is_splat_shaped() was incidentally exercised, and .splat, .spz, rebuild_format_state() and the format_override input had no test at all. Five cases added to the existing AssetLoader section, all inputs generated in-test the way the file already does (splat-shaped ASCII PLY built column by column, hand-packed 32-byte .splat rows, .spz via spz::saveSpz): * a splat-shaped .ply routes to the cloud path (cloud payload, no mesh, 3dgs.classic format, basename label) while xyz + face stays on the miniply mesh path -- both sides of the fork, explicitly; * .splat and .spz dispatch to parse_splat_binary / parse_spz and produce a one-node scene labelled with the basename, with a non-multiple-of-32 .splat and an undecodable .spz rejected cleanly; * suffix matching is case-insensitive (.SPLAT, .PLY) through hasSuffixCI; * the three-stage m_parsed -> m_overridden -> m_wrapped contract: setting format_override rewrites the cloud format, bumps the version, shares the raw_data payload, rebuilds the wrap, and leaves m_parsed_state intact so clearing the override restores the autodetected format exactly; * a mesh-path asset with an override set still clones the state but keeps its tree identity, since there is no cloud to rewrite. Seen RED four ways, reverted after each: forcing ply_is_splat_shaped to false (1 assert); disabling the .splat and .spz branches (4); making rebuild_format_state overwrite m_parsed_state (4); making the suffix match case-sensitive (2). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE (cherry picked from commit 7e4f5d7053a5b009787064cc15b1c213956cd547)
…with CTest Three suites were built but never run. CaptureAdjustShaderTest (score-plugin-gfx/CMakeLists.txt) has been an add_executable with no add_test since it landed, unlike EncoderTester and ReadbackTester twenty lines below it. It skips cleanly when no RHI is available, so it gets the same "gui" label and environment as its neighbours. It passes on this machine: OpenGL/GLSL 330, 28 checks, 0 failures, worst channel delta 1/255. be7dc83 / 46322c3 / 72a02e4 build a dual-backend render-regression sweep (render-sweep.sh, csf-sweep.sh) that no CMake file referenced. Both are now opt-in CTest entries labelled "gui", RUN_SERIAL, skipping with 77 when the gallery binary, an X display or the shader corpus is missing, and taking the built binary's path through OBJECT_GALLERY. Registering them turned up two reasons the quoted corpus verdicts could not have come from this code: * render-sweep.sh passed its per-backend environment as bare VAR=VALUE words expanded from "$@". Bash decides assignment-prefix status at parse time, so LIBGL_ALWAYS_SOFTWARE=1 became the command name and the entire llvmpipe column was a fabricated exit 127 -- 256 of 256 objects. 72a02e4 made exactly this fix to csf-sweep.sh and missed its sibling. Fixed with the same `env` prefix. * ObjectGallery's "no texture output" path returned through qApp->exit(3), and the MinimalGUIApplication teardown aborts in ApplicationContext::interfaces<Process::MagnetismAdjuster>() while destroying the ScenarioDocumentPresenter. Every geometry-only object was therefore counted as SIGABRT rather than as a skip. It now takes the same hard _Exit the success path already takes, with the abort recorded in a comment; the underlying teardown defect is untouched. With both fixed the real figure over the 256 registered objects is ok=25 skip=228 crashes=3 on each backend (CLAP: SIGTRAP; LV2 and VST: 40s timeout in the plugin scanner), so the sweep exits 1 as shipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE (cherry picked from commit e0004da3406f49fefcba0cb115e82a676e15ea59)
Same defect as the --render "no texture output" path fixed alongside the sweep registration: qApp->exit(N) runs the MinimalGUIApplication teardown, which aborts in ApplicationContext::interfaces<Process::MagnetismAdjuster>() while destroying the ScenarioDocumentPresenter, so the process dies with SIGABRT before it can return N. For --shader that turned all 21 geometry-producing compute shaders in the csf-testers corpus -- the ones 72a02e4's message documents as needing a downstream raster stage -- into FAIL(134) instead of skip(3), and csf-sweep.sh exited 1 on a corpus with nothing actually wrong with it. Exit codes 5 (unsupported extension), 1 (no such process) and 3 (no texture output) now take the same std::_Exit + coverage flush the success path already took. Also moves the <cstdlib> include out of the file header comment into the include block. The teardown abort itself is untouched: it is an application-level defect that would fire for any MinimalGUIApplication tool closing a scenario document normally. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE (cherry picked from commit ef73371da1e45a393212890c08625583dcb34b77)
…o expected-RED guards Four bodies of threedim test work that all register into tests/threedim/CMakeLists.txt and depend on each other's sources, kept as one commit so no intermediate state registers a target whose source arrives later in the branch. - the pure scene_state -> scene_state algebra (SceneSwitch, CameraSwitch, SceneGroup, SceneSelector, SceneDuplicator, TagAs), which had zero references anywhere under tests/ (52104d2, f92f98c, f0a202a) - CameraArray's six cubemap faces, the producer side of #163 - Transform3D composition - source-text guards for the four fisheye projections and the combo indices that select them (7b2704d, 0a314b5) - ShadowCascadeSetup's cascade math: split scheme, findActiveCamera id filtering, the reverse-Z NDC flip and the zPad direction (f92f98c) - two EXPECTED-RED guards, isolated in their own target so an open defect does not mask the green suites and the entry flips to PASS the day it is fixed: * #163, the `camera` auxiliary published with a one-entry byte_size * CameraSwitch's Blend accumulating onto default-constructed components, so blending a single camera at weight 1.0 -- the identity -- returns yfov + pi/4, zfar + 1000, scale 2 Every guard was seen red before it was trusted; the controls are transcribed in ~/ossia/gapmap/written-wtest2.md and written-wtest3.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Gfx/Window was the largest wholly-uncovered block of score-plugin-gfx: OutputMapping.cpp (852 instrumented lines) and DesktopLayout.cpp (401) were both at 0.0% with no reference to either name anywhere under tests/. Neither needs a GPU. The serializers are plain data; the canvases are QGraphicsView subclasses whose logic is coordinate mapping, snapping, clamping, lock modes, re-indexing and warp, all reachable by synthesising QGraphicsSceneMouseEvent / QGraphicsSceneHoverEvent and handing them to QGraphicsScene::sendEvent — the same path the scene itself uses — plus QMouseEvent to the view's viewport for the warp handle drag. test_unit_output_mapping_serialization pins the JSON and DataStream forms of Gfx::OutputMapping: both sides of every omit-if-default branch, the CornerWarp identity predicate corner by corner, the pre-LockMode Locked/LockSizeToInput compatibility path, and the wrong-arity array guards. test_gfx_window_layout pins the two canvases: updateAspectRatio and the UV invariance of the rescale it performs, setMappings/getMappings round-trip, add/remove/re-index, snap on/off/zero-distance/out-of-threshold, clamping to the canvas rather than to the warp-margin scene rect, edge resize with its minimum-size and scene bounds, the four blend handles with their 0..0.5 clamp and the ctrl precision factor, all four lock modes, the hover cursor table, warp enter/exit/toggle/reset and the four warp handles, desktop<->scene coordinate inverses, detectScreen, and updateItem's deliberate detachment of the geometry callback. Both files compile the plugin translation units through score_plugin_hidden_sources(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Gfx/Graph/decoders/ColorSpace.hpp (1387 lines) and Tonemap.hpp (~600) are header-only families of pure functions of a Video::ImageFormat, and they decide the GLSL that every YUV sampling shader in the plugin is built from. Both were entirely uncovered: their only consumers are the decoders/*.hpp samplers, which need a GPU and a decoded frame, so the decision tree was never reached even though the decision itself needs neither. test_unit_colorspace_shader sweeps colorMatrix() over every AVColorSpace and every range, and pins: the RGB passthrough; each matrix-coefficient set against the SCORE_GFX_CONVERT_* macro it must return (limited, full, and the fact that only AVCOL_RANGE_JPEG counts as full); BT.709 coefficients over P3 primaries rerouting to the P3 pipeline; BT.2020 CL being approximated by NCL; the three BT.2100 encodings (NCL / SMPTE 2085 / ICtCp) being distinct programs; ICtCp choosing its LMS inverse from the transfer function; the four output formats being four distinct programs; the SDR path carrying the resolved tonemapper; HLG in SDR running the OOTF and branching on the tonemapper's gamut class; passthrough applying no EOTF; the whole H.273 chroma-derived (12 and 13) delegation to primaries; and the unspecified fallback down to the 1280-pixel width heuristic. Also pins the numeric helpers the HDR pipelines are built on: MaxCLL and the mastering-display luminance with their plausibility windows and the zero-denominator guard, the PQ 10000/peak normalisation factor, and the BT.2408 203-nit reference white. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
GpuCapabilities.hpp holds the rung-selection logic every vendor video strategy (AJA, DeckLink, Magewell, Rivermax) consults before deciding how frames reach the GPU. The probe needs a driver; the decisions taken on its result are constexpr predicates over a plain struct and are checkable on any host. GpuCapabilities.cpp was at 0.0% coverage. The distinctions pinned are the ones that degrade silently rather than fail: CUDA VMM and nvidia_peermem are each insufficient alone for tier 0; DVP entry points without the library (and the library without entry points) are not tier 1; AMD bus-addressable memory is tier 0 and NOT tier 2 even though it makes amd.any() true; and rulesOutNvidiaPaths() must return false for GpuVendor::Unknown, since flipping that would disable DVP on every host whose vendor probe comes back empty -- Mesa's GL driver among them. Also covers gpuVendorName / qrhiBackendName over every enumerator plus an out-of-range value, and asserts of probeContextFree() that it is idempotent, NUL-terminates both fixed-size name buffers, reports the OS it was built for, and leaves the GL-only fields (amd.*, backend) untouched -- claiming those without a context would send a strategy down an unverified rung. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Gfx/Window/TestCard.cpp (renderTestCard) is the fallback pattern the multi-window output device shows when no source is routed, and was at 0.0% coverage. It is a pure QImage producer: no GPU, no window. Pins the degenerate-size guard on both axes, the returned geometry and RGB32 format, determinism across two calls, that every layer actually painted (the grey ramps alone put the distinct-colour count well past a bare fill, and the rainbow strip row carries at least eight), and that the qMax floors every layer sizes itself with survive 1x1, 3x2 and extreme aspect ratios. Added to test_gfx_window_layout rather than a new target: same subsystem, same APP fixture, same hidden-sources list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
createGPUVideoDecoder() is SCORE_PLUGIN_GFX_EXPORT and takes only a Video::ImageFormat -- no QRhi, no decoder, no frame. The whole pixel-format to decoder-class switch in GPUVideoDecoderFactory.cpp (0.0% covered) is therefore checkable on any host; only init(), which builds the shaders and textures, needs a render list, and the L3 GPU suites cover that end. Where the target is one of the two generic decoders the class proves nothing (a dozen formats share PackedDecoder), so those rows assert the QRhi texture format, the bytes-per-pixel and the swizzle filter the factory chose -- which is the entire content of the decision. Includes the 64x/16x left-alignment scaling for 10- and 12-bit planar RGB, which is a brightness bug rather than a crash when swapped; that the caller's filter is appended and not replaced; that the byte-swapped twins (UYVY/YUYV, RGB/BGR, ARGB/ABGR) do not collapse onto one another; the fourcc default branch that reinterprets the enum's four bytes as a HAP/DXV codec tag, including an unknown tag yielding nullptr; and that an unsupported format returns nullptr rather than an EmptyDecoder that would silently render nothing. Also pins PixelFormatInfo::fromAVPixelFormat and ::fromCodecParameters: subsampling, depth, plane count, alpha, the sw_format-over-codecpar preference, and that bits_per_raw_sample only ever raises the depth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
…owNode Everything under tests/gfx/ renders into an offscreen BackgroundNode and reads it back, so score::gfx::Window (0/255 lines), ScreenNode and MultiWindowNode — every path that exists only because a real platform surface with a swap chain sits in front of the renderer — were unreachable. A prior session concluded they were unreachable "by construction"; they are not, they just need a display. The new rig terminates the same score::gfx::Graph on a ScreenNode or a MultiWindowNode instead of a BackgroundNode, pumps the Qt event loop until the platform exposes the surface, and drives the manual-render path the app's non-vsync timer drives. Registered GUI, so the targets are labelled "gui" and a displayless CI excludes them; every case SKIPs cleanly with no windowing system and per RHI backend. Covered: expose / swap-chain creation, resize -> createOrResize -> renderSize mirroring, the render-size override and its degenerate-size reset, hide and re-show (the m_notExposed latch), releaseSwapChain/resizeSwapChain, the device-lost latch and its once-only deferred callback, the key/mouse/interactive event fan-out including auto-repeat filtering, the DeferredDelete swallow, the swapchain flag/format and graphics-API teardown-and-rebuild, start/stopRendering, the vsync-callback kick on the null->non-null transition, and cursor/title/ position/screen setters. For MultiWindowNode: one swap chain per mapping, the mapping seeding, every live parameter setter with its out-of-range guards, the offscreen-target recreation on a render-size change, per-window close and re-expose while the siblings keep running, the renderBlack fallback, and the empty-mapping no-op. The teardown cases run on a Graph-less rig on purpose: see the FINDING in ScreenOutputFindings.cpp.
The .sh integration harnesses need flock, oscsend and POSIX tooling. They were registered unconditionally, so on Windows ctest cannot execute them at all and reports BAD_COMMAND — which counts as a failure rather than a skip, and made 5 of the 31 reported Windows failures pure noise. Gate the registrations on SCORE_HAS_SHELL_HARNESS. On Linux and macOS nothing changes: all six are still registered and still skip via SKIP_RETURN_CODE 77 when their prerequisites are missing.
Gfx/Window/OutputPreview.cpp (403 lines, 0%) is pure QPainter work on a plain QWidget, so QWidget::grab() renders it through paintEvent offscreen and the result is checkable pixel by pixel — no GPU, no display. Covered: the four preview contents (black, per-output test card, global test card with and without a card supplied, output identification and its colour table), the source-rect window into the global card, the soft-edge blend overlay on each of the four sides, the corner warp and the rotation/mirror transform (including the "no transform requested" fast path), the fullscreen double-click toggle and the index it reports, and OutputPreviewWindows' window-per-mapping bookkeeping: growth, shrink, geometry sync on and off, fullscreen mappings, out-of-range screen indices, content propagation and the shared test-card rebuild on an input-resolution change. The blend assertions are analytic rather than "it changed": the overlay mirrors the shader's content * pow(t, gamma), so a flat identification colour makes the expected value computable, and the gamma is pinned by comparing gamma 1 against gamma 3 at the same t.
Gfx/Window/WindowSettingsWidget.cpp (1161 lines, 0%) is the only place a Gfx::WindowSettings is built from, and pushed back into, the UI. It has no accessor for any of its controls, so the test addresses them the way a user does — by the label of the QFormLayout row they sit in — which keeps the assertions readable and makes a renamed row fail loudly instead of drifting silently onto a neighbouring spin box. Covered: the defaults, the full settings round-trip (mode, swapchain flag and format, input resolution, three mappings), mode switching and the preview windows it creates and destroys, the flag/format combos, the per-output inspector (selection, window position/size/fullscreen, the source-rect spin boxes rewriting the mapping quad, all eight blend fields, rotation and mirroring, the four lock modes and the enable/derive rules they impose on the size fields, and the screen combo's Default offset), the input resolution, and the add/remove-output path with its selection semantics. Runs offscreen under run_in_app; no GPU and no display.
Gfx/Libav/LibavSettingsWidget.cpp (781 lines, 0%) and Gfx/Libav/LibavPresets.cpp (301, 0%). Most of the widget is libav introspection rather than plumbing: the muxer choice filters the encoder lists to what that muxer accepts, an encoder choice repopulates the pixel/sample format lists from that codec's declared configuration, and the validation label comes from av_guess_format / avcodec_find_encoder_by_name. Covered: the direction switch and its stacked pages, the key=value option text parser in both directions (trimming, blank lines, a line with no '=' and a line with two), every output field including all five input-transfer entries and their AVColorTransferCharacteristic values, the muxer -> encoder narrowing (and that clearing the muxer does NOT restore the full lists — onMuxerChanged returns early on an empty name), the encoder -> format repopulation with the previous selection preserved across a compatible switch and dropped when the encoder is cleared, the validation label, the ffmpeg options dialog, the settings round-trip and toOutputSettings, and every preset the enumerators publish. The libav build decides which muxers and codecs exist, so each case SKIPs rather than fails when the one it needs is absent.
Gfx/Window/WindowDevice.hpp (406 branches, 0%) and MultiWindowDevice.hpp (420,
0%) are where the score::gfx sinks become remote-controllable, and neither is
reachable without building the device through the real protocol factory on a
real document. The test does exactly that — WindowDevice{settings, doc.context()}
+ reconnect() — and then drives the published addresses.
Single window: the whole address surface is enumerated (screen, position, size,
rendersize, fullscreen, fps, cursor scaled/gl/absolute/visible, the eight tablet
axes, key press/release code and text), /size and /position are pushed and read
back off the QWindow, a window move is checked to travel back into /position
through the re-entrancy lock, /cursor/visible is toggled both ways, and a key
press and release are sent to the window and read out of the parameters.
Multi window: the per-window subtrees are enumerated and checked to stop at the
mapping count, /i/source/position and /i/source/size are shown to address the
two halves of one QRectF, /i/blend/<side>/{width,gamma} to be independent (each
carries the other over rather than resetting it), /rendersize to resize the
shared offscreen target and /i/size to resize that window only.
Also adds a second FINDING to the isolated target: a window resize discards the
render-size override, because RenderList::resizeSwapchainSizedTargets writes the
swapchain size straight into RenderState::renderSize without consulting the
output. Setting it works; the first resize afterwards throws it away.
stringIsScript scans for the punctuation inline source always carries, but the
scan was dead code: the function ended in an unconditional `return true`, so
anything that is not an existing file was treated as a program. A missing path
therefore never reached the "cannot open" branch — /definitely/does/not/exist.js
parses as a regex literal with invalid flags and surfaced as a SyntaxError at
exit 3, rather than the exit 2 the failure-reporting commit describes.
Return false when the scan finds nothing, so the input falls through to the file
branch and is reported as unopenable.
The existing test pinned the old behaviour and said so ("Tighten this to == 2 if
that is ever fixed"); it now asserts exit 2 and the diagnostic. A second section
pins the case this could plausibly have broken: inline source with no path-like
shape still runs.
This is deliberately a commit at the tip rather than a fixup into the commit that
introduced the defect, which sits at stack index 3. It is a one-line behavioural
change that cannot affect whether any commit builds, and the per-commit walks in
flight measure buildability — a fixup there would have invalidated every verified
row for no gain that those walks can observe.
Vulkan has validation layers, Metal has MTL_DEBUG_LAYER, and D3D has the debug layer (reachable in Release since the SCORE_GPU_VALIDATION change). OpenGL had nothing, which means every "no errors on GL" result so far says only that nothing was listening. Install a QOpenGLDebugLogger on the context when SCORE_GPU_VALIDATION is set, requesting QSurfaceFormat::DebugContext so KHR_debug is actually available. Logging is synchronous, so a message is emitted at the offending call and a backtrace points at the real site rather than somewhere later in the frame. Driver notification chatter (buffer placement hints and the like) stays at debug level so a genuine warning is not buried in it; SCORE_GPU_VALIDATION=2 promotes everything. First run already reports a real one, in three separate tests: glClear() called with GL_STENCIL_BUFFER_BIT, but there is no stencil buffer. Operation will have no effect. which comes from Qt pairing the depth and stencil clear bits unconditionally (qrhigles2.cpp) against the depth-only D32F attachments created in Utils.cpp. Harmless, but it is exactly the class of thing that was invisible before.
The pipewire / gstreamer / libav tests were all probe-gated: absent device -> ctest SKIP. That is right for a CI container, but on a host that HAS the media stack it means "the device was absent" quietly explains away an untested code path, and nobody notices the coverage was never there. Add a provisioning wrapper and a companion registration helper for tests that are allowed to assume a capable host: with-virtual-media.sh [--video] [--media] -- <harness> [args...] --video publishes a PipeWire Video/Source from videotestsrc through pipewiresink, which needs no root and no v4l2loopback, and waits for the node to actually appear before running anything. --media generates H.264 and raw clips with ffmpeg. Both are torn down on exit, including on failure. score_add_media_test() registers these WITHOUT SKIP_RETURN_CODE, so a missing dependency fails the run. Hosts that genuinely cannot provide the stack set SCORE_MEDIA_TESTS_OPTIONAL=1 and get the old skip back. Registered Media_PipewireRoundtrip_input as the first of these; it passes against the virtual source in ~30s.
--media generated two clips and had no consumer. It now generates one rawvideo-in-NUT clip per pixel format the decoders claim to handle -- the only container that carries an arbitrary pix_fmt through untouched -- at even and at ODD sizes, plus the three heights that trigger each branch of VideoDecoder's colour-space inference, an audio+video mp4, and three malformed inputs. The explicit scale filter is load-bearing: a lavfi source silently rounds an odd requested size DOWN to even, so testsrc2=size=65x33 yields a 64x32 clip and the odd-dimension axis quietly disappears. The geometry is read back with ffprobe and a mismatch is fatal, so a clip cannot claim a size it does not have. score_add_test() grows NO_CTEST for a harness whose ctest entry has to go through score_add_media_test(): without it the harness would also be registered as a bare entry that runs with none of the media provisioned.
…ulary Video/VideoDecoder.cpp was at 6% of 482 lines, GpuFormats.hpp at 0% of 126, FrameQueue.cpp at 13%, Rescale.cpp at 7%, Thumbnailer.cpp at 12%. Nothing in the suite opened a video file at all. LibavDecodeTest runs VideoDecoder over the whole clip matrix the media runner provisions: metadata against an independent avformat probe of the same file, one frame per pixel format with every declared plane checked for a real pointer and a wide-enough stride, odd dimensions, the swscale path (which is reached only by the formats formatNeedsDecoding() claims, and whose output must be varied and opaque rather than a zeroed buffer), the height-driven colour-space inference, video-stream selection out of a multi-stream file, seek forward / back to zero / past the end, reload onto another file, malformed inputs, the DecoderConfiguration knobs and the raw packet route. Registered through the strict provisioning wrapper, so an absent ffmpeg is a failure and not a silently missing suite. VideoFrameQueueTest pins the ownership boundary between the decode and render threads: pool recycling, latest-frame-wins vs single-step dequeue, and the seek discard marker -- including the guard that must NOT return a marker frame the queue no longer holds, which would double-own the pixels a live GPU upload is reading. GpuFormatsTest sweeps the hardware-decoding vocabulary, and crosses it with score::gfx::createGPUVideoDecoder(): "no rescale needed" and "here is your GPU decoder" are two independently maintained switches over AVPixelFormat, each self-consistent, and a format only one of them knows renders nothing at all. Two [!shouldfail] cases record defects rather than assert them: VideoThumbnailer::process() dereferences a null format context on a file it could not open (run in a forked child so the crash cannot take the binary down), and the big-endian grayscale floats skip swscale with no decoder waiting for them.
GStreamerDevice.cpp was at 4% of 1010 lines and GStreamerLoader.hpp at 0%. The device runs its own pipeline, so videotestsrc is producer and reference at once and no external process is needed: gst_parse_launch -> appsink classification -> FrameQueue -> AVFrame is assertable on the CPU by scaling one decoded frame down to a single RGBA pixel, with no GPU and no display. Solid patterns through packed RGB, planar and semi-planar YUV and packed 4:2:2, plus the pipeline-error paths and the preset enumerators. This harness existed and was shelved as "hangs over 12 configurations". It was never GStreamer: PM::PluginSettingsModel::firstTimeLibraryDownload() opens a modal question box one second after boot, the harness pumps the event loop while waiting for frames, and QDialog::exec() never returns under the offscreen QPA. The ctest entry sets the existing escape hatch. The second half of the fix is start_execution(): reconnect() only brings the pipeline to PAUSED, so without it the appsink never yields a sample and every case reads back nothing. The suite now runs in 19 seconds. Two [!shouldfail] cases record what the sweep found: no 10-bit planar format produces a single frame (initFrameFromRawData() refuses every high-bit-depth planar layout, though both the caps table and the GPU decoders know them), and a caps format the table does not know is reinterpreted as RGBA instead of refused -- BGRx turns blue into red, Y42B turns red into grey.
Video/LibavStreamInput.cpp was at 0% of 338 lines. It is a different object from VideoDecoder with its own demux thread, its own audio path and its own pacing decision, and a local file is a legitimate URL for it, so none of this needs a network or a capture device: probe metadata against an independent avformat read of the same file, frame delivery, start/stop/start, score's own "loop" option (the clip is 8 frames long, so more than 8 frames out is only possible if it wrapped), a lavfi generator, and the audio stream reaching AudioRingBuffer. The ring buffer itself is swept over all three writers plus the empty-read case. A [!shouldfail] case records the defect the sweep turned up: with no options -- the default, and what a file path typed into the FFmpeg input device produces -- probe() applies AVFMT_FLAG_NOBUFFER, fflags=nobuffer and flags=low_delay to every source, and a local file then delivers ZERO frames. probe() and start() both succeed; nothing comes out. Passing any option at all, even one as inert as probesize, takes probe() down its other branch and the same file plays.
…ly have
createRenderTarget's MRT overload allocates one multisample colour
renderbuffer per attachment and resolves each into the caller's
single-sample texture, but it kept those renderbuffers in a local: the
TextureRenderTarget it returned only ever recorded the resolve textures.
TextureRenderTarget::sampleCount() therefore answered 1 -- the resolve
target's count -- for a render target whose attachments are N-sample, and
every pipeline built against it (buildPipeline, buildPipelineWithState,
RenderedRawRasterPipelineNode::initMRTPass) was created with
setSampleCount(1).
D3D12 calls that out per draw:
ID3D12CommandList::DrawInstanced: The render target sample desc in
slot 0 does not match that specified by the current pipeline state.
(pipeline state = count 1 quality 0, render target view = count 2
quality 0, ID3D12Resource* = 'createRenderTarget::MRT::colorRB')
394 such messages across the gfx suite under the D3D12 debug layer, all
of them naming that renderbuffer, from the three MRT tests (test_gfx_isf
192, test_gfx_isf_mrt 162, test_gfx_isf_mrt_persistent 40). A pipeline
state whose SampleDesc disagrees with the bound RTV is undefined
behaviour, not a hint.
The renderbuffers were also leaked: release() and the failure path both
walk the render target's members, and neither could see them.
Record them on the render target -- the first as colorRenderBuffer, which
is what sampleCount() consults first, the rest alongside the additional
colour textures -- so the pipelines agree with the attachments and the
buffers are released with the rest of the target.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Every render target built by createRenderTarget(format, size, ...) was
allocated MipMapped | UsedWithGenerateMips, and every 2D CSF storage
image likewise, whether or not anything would ever fill the chain. The
call sites that do generate mips (TextureLoader, the GENERATE_MIPS output
path in RenderedRawRasterPipelineNode, and the CSF path guarded by
si.generate_mips) allocate their own textures and pass the flags
themselves; the only other producer is RenderedCSFNode::update(), which
regenerates the mips of its input samplers -- and skips any texture that
is not MipMapped.
So for every render target whose consumer does not filter across mip
levels, levels 1..N were never written. The shader resource view a
sampler binds covers the whole chain regardless, and D3D12 reports it:
ID3D12CommandQueue1::ExecuteCommandLists: ... resources with
D3D12_HEAP_FLAG_CREATE_NOT_ZEROED flag with either render target or
depth stencil flags must be initialized with a Discard/Clear/Copy
operations before other operations are supported. Resource
('createRenderTarget::texture'), Subresource (3) is not initialized
but is used in SetGraphicsRootDescriptorTable
203 of those across the gfx suite name createRenderTarget::texture,
covering subresources 1 through 6 and never 0 -- exactly the mips, never
the level the render pass writes -- and 30 more name a CSF storage image.
Make the chain follow the demand for it. RenderList allocates it when the
port's resolved sampler spec has a mipmap mode, which is the case that
CSF's regeneration serves; the CSF storage images follow the
GENERATE_MIPS declaration that already gates their generateMips() call.
Everywhere else the texture is single-level, and a sampler with a mipmap
filter over it reads level 0 -- the only level that ever held data.
Both also gain UsedAsTransferSource, which they always needed and were
getting by accident: Qt's Vulkan backend derives
VK_IMAGE_USAGE_TRANSFER_SRC_BIT from UsedAsTransferSource *or*
UsedWithGenerateMips (qrhivulkan.cpp), so dropping the latter took the
readback path's usage bit with it -- 1222 validation lines on the Vulkan
gfx suite, VUID-vkCmdCopyImageToBuffer-srcImage-00186 and the layout
transitions that follow it, against 26 before. Declaring the flag score
actually relies on puts that back at 26. No D3D or GL backend reads the
flag at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The PrimitiveCloud point-cloud / Gaussian-splat family — the last piece of the 3D-object work, deferred from #2122 because its build wiring is more involved.
Stacked on #2122; retargets as the stack merges.
What it adds
.splatbinary reader, an SPZ codec for compressed 3D Gaussian splats (via newspz+zstdsubmodules), a scene-from-cloud node, and a format override.TagAshelper, and asset-loader routing for.ply/.splat/.spz.Kept, not retired (per the "keep both" decision)
The upstream 3D branch deletes the old
Splat/render node in favour of this family; here it is preserved — the Splat render node and Splat loader remain registered alongside PrimitiveCloud.Notes
The earlier deferral cited three blockers; two dissolved once built from the branch tip rather than an intermediate commit (the
Gfx::FormatRegistryreference is an empty-stub include at tip, unused), and thelibspz-fPIClink failure was from applying the zstd build wiring incompletely — the complete3rdparty/zstd.cmake+ redirect + spz stub build cleanly.Validation
ctest: 17/17.🤖 Generated with Claude Code